Skip to content

Replace download_url with /file endpoint#478

Open
dima-aignostics wants to merge 4 commits intomainfrom
feat/PAPI-4760-stop-using-download_url
Open

Replace download_url with /file endpoint#478
dima-aignostics wants to merge 4 commits intomainfrom
feat/PAPI-4760-stop-using-download_url

Conversation

@dima-aignostics
Copy link

No description provided.

@codecov
Copy link

codecov bot commented Mar 16, 2026

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
733 3 730 15
View the top 3 failed test(s) by shortest run time
tests.aignostics.application.utils_test::test_retrieve_and_print_run_details_default_is_detailed
Stack Traces | 0.006s run time
mock_console = <MagicMock name='console' id='140038505405408'>

    @pytest.mark.unit
    @patch("aignostics.application._utils.console")
    def test_retrieve_and_print_run_details_default_is_detailed(mock_console: Mock) -> None:
        """Test that default mode (summarize=False) shows detailed output with artifacts."""
        terminated_at = datetime(2025, 1, 1, 13, 0, 0, tzinfo=UTC)
    
        run_data = _make_run_data(
            run_id="run-detailed-test",
            application_id="he-tme",
            version_number="1.0.0",
            state=RunState.TERMINATED,
            termination_reason=RunTerminationReason.ALL_ITEMS_PROCESSED,
            output=RunOutput.FULL,
            statistics=_make_statistics(item_count=1, item_succeeded_count=1),
            terminated_at=terminated_at,
        )
    
        item_result = _make_item_result(
            item_id="item-123",
            terminated_at=terminated_at,
            output_artifacts=[_make_artifact()],
        )
    
        mock_run = MagicMock()
        mock_run.details.return_value = run_data
        mock_run.results.return_value = [item_result]
    
        # Call without summarize parameter (default is False)
        retrieve_and_print_run_details(mock_run, hide_platform_queue_position=False)
    
        all_output = " ".join(str(call) for call in mock_console.print.call_args_list)
    
        # Verify detailed output shows "Run Details" not "Run Summary"
        assert "Run Details for run-detailed-test" in all_output
        # Verify artifact details ARE shown in detailed mode
>       assert "Download URL" in all_output
E       assert 'Download URL' in "call('[bold]Run Details for run-detailed-test[/bold]\\n================================================================================\\n[bold]Run ID:[/bold] run-detailed-test\\n[bold]Application (Version):[/bold] he-tme (1.0.0)\\n[bold]Queue Position:[/bold] N/A\\n[bold]Status (Termination Reason):[/bold] TERMINATED (RunTerminationReason.ALL_ITEMS_PROCESSED)\\n[bold]Output:[/bold] FULL\\n[bold]Statistics:[/bold]\\n  - 1 items\\n  - 0 pending\\n  - 0 processing\\n  - 0 skipped\\n  - 1 succeeded\\n  - 0 user errors\\n  - 0 system errors\\n[bold]Submitted (by):[/bold] 2025-01-01 12:00:00+00:00 (user@example.com)\\n[bold]Terminated (duration):[/bold] 2025-01-01 13:00:00+00:00 (1 hour)\\n[bold]Custom Metadata:[/bold] None\\n\\n[bold]Items:[/bold]') call('  [bold]Item ID:[/bold] item-123\\n  [bold]Item External ID:[/bold] `slide-001.svs`\\n  [bold]Status (Termination Reason):[/bold] TERMINATED (ItemTerminationReason.SUCCEEDED)\\n  [bold]Error Message (Code):[/bold] None (None)\\n  [bold]Custom Metadata:[/bold] None\\n  [bold]Output Artifacts:[/bold]\\n    - Name: result.parquet\\n      MIME Type: application/vnd.apache.parquet\\n      Artifact ID: artifact-abc\\n')"

.../aignostics/application/utils_test.py:903: AssertionError
tests.aignostics.qupath.gui_test::test_gui_run_qupath_install_to_inspect
Stack Traces | 318s run time
user = <nicegui.testing.user.User object at 0x7f23181b3f00>
runner = <typer.testing.CliRunner object at 0x7f2320854130>
tmp_path = PosixPath('.../pytest-of-runner/pytest-21/test_gui_run_qupath_install_to0')
silent_logging = None, qupath_teardown = None, qupath_save_restore = None
record_property = <function record_property.<locals>.append_property at 0x7f231b1a1b10>

    @pytest.mark.e2e
    @pytest.mark.long_running
    @pytest.mark.skipif(
        (platform.system() == "Linux" and platform.machine() in {"aarch64", "arm64"}),
        reason="QuPath is not supported on ARM64 Linux",
    )
    @pytest.mark.timeout(timeout=60 * 15)
    @pytest.mark.sequential
    async def test_gui_run_qupath_install_to_inspect(  # noqa: C901, PLR0912, PLR0913, PLR0914, PLR0915, PLR0917
        user: User,
        runner: CliRunner,
        tmp_path: Path,
        silent_logging: None,
        qupath_teardown: None,
        qupath_save_restore: None,
        record_property,
    ) -> None:
        """Test installing QuPath, downloading run results, creating QuPath project from it, and inspecting results."""
        record_property("tested-item-id", "TC-QUPATH-01, SPEC-GUI-SERVICE")
    
        # Find run
        runs = Service().application_runs(
            application_id=HETA_APPLICATION_ID,
            application_version=HETA_APPLICATION_VERSION,
            external_id=SPOT_0_GS_URL,
            tags=["scheduled"],
            has_output=True,
            limit=1,
        )
        if not runs:
            message = f"No matching runs found for application {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION}). "
            message += "This test requires the scheduled test test_application_runs_heta_version passing first."
            pytest.skip(message)
    
        run_id = runs[0].run_id
    
        # Explore run
        run = Service().application_run(run_id).details()
        print(
            f"Found existing run: {run.run_id}\n"
            f"application: {run.application_id} ({run.version_number})\n"
            f"status: {run.state}, output: {run.output}\n"
            f"submitted at: {run.submitted_at}, terminated at: {run.terminated_at}\n"
            f"statistics: {run.statistics!r}\n",
            f"custom_metadata: {run.custom_metadata!r}\n",
        )
    
        # Explore results
        results = list(Service().application_run(run_id).results())
        assert results, f"No results found for run {run_id}"
        for item in results:
            print(
                f"Found item: {item.item_id}, status: {item.state}, output: {item.output}, "
                f"external_id: {item.external_id}\n"
                f"custom_metadata: {item.custom_metadata!r}\n",
            )
    
        with patch(
            "aignostics.application._gui._page_application_run_describe.get_user_data_directory", return_value=tmp_path
        ):
            # Step 1: (Re)Install QuPath
            result = runner.invoke(cli, ["qupath", "install"])
            output = normalize_output(result.output, strip_ansi=True)
            assert f"QuPath v{QUPATH_VERSION} installed successfully" in output, (
                f"Expected 'QuPath v{QUPATH_VERSION} installed successfully' in output.\nOutput: {output}"
            )
            assert result.exit_code == 0
    
            # Step 2: Go to latest completed run via GUI
            await user.open(f"/application/run/{run.run_id}")
            await user.should_see(f"Run {run.run_id}")
            await user.should_see(f"Run of {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION})")
    
            # Step 3: Open Result Download dialog
            await user.should_see(marker="BUTTON_OPEN_QUPATH", retries=100)
            user.find(marker="BUTTON_OPEN_QUPATH").click()
    
            # Step 4: Select Data destination
            await user.should_see(marker="BUTTON_DOWNLOAD_DESTINATION_DATA")
            download_destination_data_button: ui.button = user.find(
                marker="BUTTON_DOWNLOAD_DESTINATION_DATA"
            ).elements.pop()
            assert download_destination_data_button.enabled, "Download destination button should be enabled"
            user.find(marker="BUTTON_DOWNLOAD_DESTINATION_DATA").click()
            await assert_notified(user, "Using Launchpad results directory", 30)
    
            # Step 5: Trigger Download
            await user.should_see(marker="DIALOG_BUTTON_DOWNLOAD_RUN")
            download_run_button: ui.button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert download_run_button.enabled, "Download button should be enabled before downloading"
            user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").click()
            await assert_notified(user, "Downloading ...", 30)
    
            # Step 6: Check download completes, QuPath project created, and QuPath launched
>           await assert_notified(user, "Download and QuPath project creation completed.", 60 * 5)

.../aignostics/qupath/gui_test.py:232: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

user = <nicegui.testing.user.User object at 0x7f23181b3f00>
expected_notification = 'Download and QuPath project creation completed.'
wait_seconds = 300

    async def assert_notified(user: User, expected_notification: str, wait_seconds: int = 5) -> str:
        """Check if the user receives a notification within the specified time.
    
        This utility function helps test GUI notifications by waiting for a specific
        notification message to appear in the user's notification messages.
    
        Args:
            user: The nicegui User instance for testing.
            expected_notification: The notification text to look for (partial match).
            wait_seconds: Maximum time to wait for the notification (default: 5).
    
        Returns:
            str: The oldest matching notification message found.
    
        Raises:
            pytest.fail: If no matching notification is found within the wait time.
        """
        for _ in range(wait_seconds):
            matching_messages = [msg for msg in user.notify.messages if expected_notification in msg]
            if matching_messages:
                return matching_messages[0]
            await sleep(1)
    
        recent_messages = (user.notify.messages[-10:] if len(user.notify.messages) > 10 else user.notify.messages)[::-1]
        total_count = len(user.notify.messages)
>       pytest.fail(
            f"No notification containing '{expected_notification}' was found within {wait_seconds} seconds. "
            f"Total messages: {total_count}. Recent messages: {recent_messages}"
        )
E       Failed: No notification containing 'Download and QuPath project creation completed.' was found within 300 seconds. Total messages: 5. Recent messages: ['#x1F389 Run he-tme completed!', '#x1F389 Run he-tme completed!', '#x1F389 Run he-tme completed!', 'Downloading ...', 'Using Launchpad results directory']

tests/conftest.py:131: Failed
tests.aignostics.application.gui_test::test_gui_run_download
Stack Traces | 488s run time
user = <nicegui.testing.user.User object at 0x7f2353405810>
runner = <typer.testing.CliRunner object at 0x7f2353406780>
tmp_path = PosixPath('.../pytest-of-runner/pytest-21/test_gui_run_download1')
silent_logging = None
record_property = <function record_property.<locals>.append_property at 0x7f231bcb9220>

    @pytest.mark.e2e
    @pytest.mark.long_running
    @pytest.mark.flaky(retries=1, delay=5)
    @pytest.mark.timeout(timeout=60 * 10)
    @pytest.mark.sequential  # Helps on Linux with image analysis step otherwise timing out
    async def test_gui_run_download(  # noqa: PLR0915
        user: User, runner: CliRunner, tmp_path: Path, silent_logging: None, record_property
    ) -> None:
        """Test that the user can download a run result via the GUI."""
        record_property("tested-item-id", "SPEC-APPLICATION-SERVICE, SPEC-GUI-SERVICE")
        with patch(
            "aignostics.application._gui._page_application_run_describe.get_user_data_directory",
            return_value=tmp_path,
        ):
            # Find run
            runs = Service().application_runs(
                application_id=HETA_APPLICATION_ID,
                application_version=HETA_APPLICATION_VERSION,
                external_id=SPOT_0_GS_URL,
                tags=["scheduled"],
                has_output=True,
                limit=1,
            )
            if not runs:
                message = f"No matching runs found for application {HETA_APPLICATION_ID} ({HETA_APPLICATION_VERSION}). "
                message += "This test requires the scheduled test test_application_runs_heta_version passing first."
                pytest.skip(message)
    
            run_id = runs[0].run_id
    
            # Explore run
            run = Service().application_run(run_id).details()
            print(
                f"Found existing run: {run.run_id}\n"
                f"application: {run.application_id} ({run.version_number})\n"
                f"status: {run.state}, output: {run.output}\n"
                f"submitted at: {run.submitted_at}, terminated at: {run.terminated_at}\n"
                f"statistics: {run.statistics!r}\n",
                f"custom_metadata: {run.custom_metadata!r}\n",
            )
            # Step 1: Go to latest completed run
            await user.open(f"/application/run/{run.run_id}")
            await user.should_see(f"Run {run.run_id}", retries=100)
            await user.should_see(
                f"Run of {run.application_id} ({run.version_number})",
                retries=100,
            )
    
            # Step 2: Open Result Download dialog
            await user.should_see(marker="BUTTON_DOWNLOAD_RUN", retries=100)
            user.find(marker="BUTTON_DOWNLOAD_RUN").click()
    
            # Step 3: Check download button is initially disabled, then select Data folder
            download_run_button: ui.button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert not download_run_button.enabled, "Download button should be disabled before selecting target"
            await user.should_see(marker="BUTTON_DOWNLOAD_DESTINATION_DATA", retries=100)
            user.find(marker="BUTTON_DOWNLOAD_DESTINATION_DATA").click()
            await assert_notified(user, "Using Launchpad results directory")
    
            # Step 4: Trigger Download - wait for button to be enabled
            download_run_button = user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").elements.pop()
            assert download_run_button.enabled, "Download button should be enabled after selecting target"
            user.find(marker="DIALOG_BUTTON_DOWNLOAD_RUN").click()
            await assert_notified(user, "Downloading ...")
    
            # Check: Download completed
>           await assert_notified(user, "Download completed.", 60 * 4)

.../aignostics/application/gui_test.py:411: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

user = <nicegui.testing.user.User object at 0x7f2353405810>
expected_notification = 'Download completed.', wait_seconds = 240

    async def assert_notified(user: User, expected_notification: str, wait_seconds: int = 5) -> str:
        """Check if the user receives a notification within the specified time.
    
        This utility function helps test GUI notifications by waiting for a specific
        notification message to appear in the user's notification messages.
    
        Args:
            user: The nicegui User instance for testing.
            expected_notification: The notification text to look for (partial match).
            wait_seconds: Maximum time to wait for the notification (default: 5).
    
        Returns:
            str: The oldest matching notification message found.
    
        Raises:
            pytest.fail: If no matching notification is found within the wait time.
        """
        for _ in range(wait_seconds):
            matching_messages = [msg for msg in user.notify.messages if expected_notification in msg]
            if matching_messages:
                return matching_messages[0]
            await sleep(1)
    
        recent_messages = (user.notify.messages[-10:] if len(user.notify.messages) > 10 else user.notify.messages)[::-1]
        total_count = len(user.notify.messages)
>       pytest.fail(
            f"No notification containing '{expected_notification}' was found within {wait_seconds} seconds. "
            f"Total messages: {total_count}. Recent messages: {recent_messages}"
        )
E       Failed: No notification containing 'Download completed.' was found within 240 seconds. Total messages: 5. Recent messages: ['#x1F389 Run he-tme completed!', '#x1F389 Run he-tme completed!', '#x1F389 Run he-tme completed!', 'Downloading ...', 'Using Launchpad results directory']

tests/conftest.py:131: Failed

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the SDK’s artifact download flow to stop relying on the deprecated download_url field and instead obtain fresh presigned URLs via the Platform’s /v1/runs/{run_id}/artifacts/{artifact_id}/file endpoint. It also regenerates the OpenAPI client to the newer API version and adds unit tests around the new download behavior.

Changes:

  • Add Run.get_artifact_download_url() and use it when downloading output artifacts (replacing artifact.download_url usage).
  • Update application download helpers to fetch a fresh presigned URL per artifact before downloading.
  • Regenerate OpenAPI spec + generated client/docs (new endpoint, scheduling fields, model updates, API version bump).

Reviewed changes

Copilot reviewed 4 out of 48 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/aignostics/application/download_test.py Adds unit tests for download_item_artifact using the new presigned-URL fetching flow.
src/aignostics/platform/resources/runs.py Adds get_artifact_download_url() and switches artifact download logic to use /file endpoint-derived URLs.
src/aignostics/application/_download.py Updates artifact downloading to request a fresh presigned URL via Run.get_artifact_download_url().
codegen/out/docs/PublicApi.md Documents new /file endpoint and updated API parameters.
codegen/out/aignx/codegen/rest.py Updates generated client metadata to API version 1.3.0+dev....
codegen/out/aignx/codegen/models/version_read_response.py Regenerated model (adds regex validator and typing changes).
codegen/out/aignx/codegen/models/validation_error_loc_inner.py Regenerated header/version update.
codegen/out/aignx/codegen/models/validation_error.py Regenerated model (adds input/ctx fields).
codegen/out/aignx/codegen/models/user_read_response.py Regenerated header/version update.
codegen/out/aignx/codegen/models/scheduling_response.py New generated model for run scheduling fields in responses.
codegen/out/aignx/codegen/models/scheduling_request.py New generated model for run scheduling constraints in requests.
codegen/out/aignx/codegen/models/run_termination_reason.py Regenerated header/version update.
codegen/out/aignx/codegen/models/run_state.py Regenerated header/version update.
codegen/out/aignx/codegen/models/run_read_response.py Regenerated model (adds scheduling).
codegen/out/aignx/codegen/models/run_output.py Regenerated header/version update.
codegen/out/aignx/codegen/models/run_item_statistics.py Regenerated header/version update.
codegen/out/aignx/codegen/models/run_creation_response.py Regenerated model (makes run_id required).
codegen/out/aignx/codegen/models/run_creation_request.py Regenerated model (adds scheduling).
codegen/out/aignx/codegen/models/output_artifact_visibility.py Regenerated header/version update.
codegen/out/aignx/codegen/models/output_artifact_scope.py Regenerated header/version update.
codegen/out/aignx/codegen/models/output_artifact_result_read_response.py Regenerated model (marks download_url deprecated, adjusts fields).
codegen/out/aignx/codegen/models/output_artifact.py Regenerated header/version update.
codegen/out/aignx/codegen/models/organization_read_response.py Regenerated header/version update.
codegen/out/aignx/codegen/models/me_read_response.py Regenerated header/version update.
codegen/out/aignx/codegen/models/item_termination_reason.py Regenerated header/version update.
codegen/out/aignx/codegen/models/item_state.py Regenerated header/version update.
codegen/out/aignx/codegen/models/item_result_read_response.py Regenerated model (adds error_code field ordering/structure changes).
codegen/out/aignx/codegen/models/item_output.py Regenerated header/version update.
codegen/out/aignx/codegen/models/item_creation_request.py Regenerated header/version update.
codegen/out/aignx/codegen/models/input_artifact_creation_request.py Regenerated header/version update.
codegen/out/aignx/codegen/models/input_artifact.py Regenerated header/version update.
codegen/out/aignx/codegen/models/http_validation_error.py Regenerated header/version update.
codegen/out/aignx/codegen/models/custom_metadata_update_response.py Regenerated header/version update.
codegen/out/aignx/codegen/models/custom_metadata_update_request.py Regenerated header/version update.
codegen/out/aignx/codegen/models/artifact_termination_reason.py Regenerated header/version update.
codegen/out/aignx/codegen/models/artifact_state.py Regenerated header/version update.
codegen/out/aignx/codegen/models/artifact_output.py Regenerated header/version update (enum values aligned to new API).
codegen/out/aignx/codegen/models/application_version.py Regenerated model (adds regex validator and typing changes).
codegen/out/aignx/codegen/models/application_read_short_response.py Regenerated header/version update.
codegen/out/aignx/codegen/models/application_read_response.py Regenerated header/version update.
codegen/out/aignx/codegen/models/init.py Regenerated exports list to include new/updated models.
codegen/out/aignx/codegen/exceptions.py Regenerated header/version update.
codegen/out/aignx/codegen/configuration.py Regenerated header/version update + debug report API version string.
codegen/out/aignx/codegen/api_client.py Regenerated header/version update.
codegen/out/aignx/codegen/api/public_api.py Regenerated API client (adds /file endpoint, updates paths/params).
codegen/out/.openapi-generator/FILES Updates generated file manifest with new scheduling models.
codegen/in/openapi.json Updates input OpenAPI spec (new endpoint, scheduling, deprecations, version bump).
codegen/in/archive/openapi_1.3.0+dev.305920c97bb.json Adds archived OpenAPI snapshot for the new API version.

Comment on lines +306 to +311
response = requests.get(
url,
headers=dict(header_params),
allow_redirects=False,
timeout=settings().run_timeout,
)
Comment on lines +312 to +323
if response.status_code == requests.codes.temporary_redirect:
location = response.headers.get("Location")
if not location:
msg = f"307 redirect received but Location header is absent for artifact {artifact_id!r}"
raise RuntimeError(msg)
return location
response.raise_for_status()
msg = (
f"Unexpected status {response.status_code} from artifact URL endpoint "
f"for artifact {artifact_id!r}; expected 307 redirect"
)
raise RuntimeError(msg)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual endpoint never returns 200

Comment on lines +446 to +447
with patch("aignostics.application._utils.get_file_extension_for_artifact", return_value=".tiff"):
download_item_artifact(progress, mock_run, artifact, tmp_path)
Comment on lines +483 to +484
with patch("aignostics.application._utils.get_file_extension_for_artifact", return_value=".tiff"):
existing_file = tmp_path / "result.tiff"
@dima-aignostics dima-aignostics force-pushed the feat/PAPI-4760-stop-using-download_url branch from c9d8ba5 to cd1c406 Compare March 16, 2026 14:23
Copilot AI review requested due to automatic review settings March 16, 2026 14:23
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates artifact downloading to stop relying on the deprecated download_url field and instead fetch fresh presigned URLs via the new GET /v1/runs/{run_id}/artifacts/{artifact_id}/file endpoint (capturing the 307 Location redirect). It also refreshes the generated OpenAPI client/spec to include the new endpoint and related schema changes.

Changes:

  • Add Run.get_artifact_download_url() and use it for artifact downloads (platform + application download flows).
  • Update unit tests to cover the new redirect-based URL retrieval.
  • Regenerate OpenAPI artifacts/spec (new endpoint, new scheduling models, schema tweaks, base-path fixes).

Reviewed changes

Copilot reviewed 6 out of 50 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/aignostics/platform/resources/runs_test.py Adds unit tests for redirect-based presigned URL retrieval via the new /file endpoint.
tests/aignostics/application/download_test.py Import formatting only (no functional change).
src/aignostics/platform/resources/runs.py Implements get_artifact_download_url() and switches run artifact downloading to use it.
src/aignostics/application/_download.py Switches per-artifact download flow to request fresh presigned URLs from the run before downloading.
codegen/out/docs/PublicApi.md Documents the new artifact URL endpoint and updated list-runs query param docs.
codegen/out/aignx/codegen/rest.py Bumps embedded OpenAPI document version string (generated).
codegen/out/aignx/codegen/models/version_read_response.py Generated semver regex validation for version_number.
codegen/out/aignx/codegen/models/validation_error_loc_inner.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/validation_error.py Generated additions for input and ctx fields.
codegen/out/aignx/codegen/models/user_read_response.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/scheduling_response.py New generated model for scheduling fields in run responses.
codegen/out/aignx/codegen/models/scheduling_request.py New generated model for scheduling constraints in run creation.
codegen/out/aignx/codegen/models/run_termination_reason.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/run_state.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/run_read_response.py Adds generated scheduling field support in run responses.
codegen/out/aignx/codegen/models/run_output.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/run_item_statistics.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/run_creation_response.py Makes run_id required in generated response model.
codegen/out/aignx/codegen/models/run_creation_request.py Adds generated scheduling field for run creation payloads.
codegen/out/aignx/codegen/models/output_artifact_visibility.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/output_artifact_scope.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/output_artifact_result_read_response.py Marks download_url deprecated in schema + adjusts ordering/optionality.
codegen/out/aignx/codegen/models/output_artifact.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/organization_read_response.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/me_read_response.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/item_termination_reason.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/item_state.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/item_result_read_response.py Adds generated error_code field and adjusts schema serialization.
codegen/out/aignx/codegen/models/item_output.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/item_creation_request.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/input_artifact_creation_request.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/input_artifact.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/http_validation_error.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/custom_metadata_update_response.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/custom_metadata_update_request.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/artifact_termination_reason.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/artifact_state.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/artifact_output.py OpenAPI version string update (generated, includes new output statuses).
codegen/out/aignx/codegen/models/application_version.py Generated semver regex validation for application version numbers.
codegen/out/aignx/codegen/models/application_read_short_response.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/application_read_response.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/models/init.py Reorders/extends generated model exports (adds scheduling models).
codegen/out/aignx/codegen/exceptions.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/configuration.py Updates debug report API version string (generated).
codegen/out/aignx/codegen/api_client.py OpenAPI version string update (generated).
codegen/out/aignx/codegen/api/public_api.py Adds new generated endpoint + fixes resource paths to avoid double /api prefix.
codegen/out/.openapi-generator/FILES Adds scheduling model files to the generator manifest.
codegen/in/openapi.json Updates source OpenAPI spec: new /file endpoint, deprecations, new fields, auth URLs.
codegen/in/archive/openapi_1.3.0+dev.305920c97bb.json Adds archived OpenAPI snapshot for the regenerated version.
CHANGELOG.md Removes trailing blank lines at end of file.

Comment on lines +741 to +746
mock_get.assert_called_once_with(
"https://api.example.com/v1/runs/test-run-id/artifacts/art-1/file",
headers={},
allow_redirects=False,
timeout=mock_get.call_args[1]["timeout"],
)
Comment on lines +749 to +778
@pytest.mark.parametrize(
("status_code", "expected_message"),
[
(200, "Unexpected status 200 from artifact URL endpoint"),
(307, "307 redirect received but Location header is absent"),
(404, "Unexpected status 404 from artifact URL endpoint for artifact 'art-1'; expected 307 redirect"),
],
)
@pytest.mark.unit
def test_get_artifact_download_url_errors(app_run, mock_serialize, status_code, expected_message) -> None:
"""Test that get_artifact_download_url raises RuntimeError after unexpected result.

Args:
app_run: Run instance with mock API.
mock_serialize: Mock serializer configured on the API.
status_code: The HTTP status code to simulate in the response.
expected_message: The expected error message to be included in the RuntimeError.
"""
# Arrange
mock_response = MagicMock()
mock_response.__enter__ = Mock(return_value=mock_response)
mock_response.__exit__ = Mock(return_value=False)
mock_response.status_code = status_code
mock_response.headers = {} # No Location header

with (
patch("aignostics.platform.resources.runs.requests.get", return_value=mock_response),
pytest.raises(RuntimeError, match=expected_message),
):
app_run.get_artifact_download_url("art-1")
Comment on lines 445 to 456
if file_path.exists():
file_checksum = calculate_file_crc32c(file_path)
if file_checksum != checksum:
logger.trace("Resume download for {} to {}", artifact.name, file_path)
print(f"> Resume download for {artifact.name} to {file_path}") if print_status else None
else:
continue
else:
downloaded_at_least_one_artifact = True
logger.trace("Download for {} to {}", artifact.name, file_path)
print(f"> Download for {artifact.name} to {file_path}") if print_status else None

Comment on lines 401 to +405
msg = f"Download operation failed unexpectedly for run {self.run_id}: {e}"
raise RuntimeError(msg) from e

@staticmethod
def ensure_artifacts_downloaded(
self,
@dima-aignostics dima-aignostics force-pushed the feat/PAPI-4760-stop-using-download_url branch from cd1c406 to 82d3c3f Compare March 17, 2026 09:31
Copilot AI review requested due to automatic review settings March 17, 2026 09:39
@dima-aignostics dima-aignostics force-pushed the feat/PAPI-4760-stop-using-download_url branch from 82d3c3f to ad8178d Compare March 17, 2026 09:39
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the SDK’s run artifact downloading flow to stop relying on the deprecated download_url field and instead obtain fresh presigned URLs via the platform’s /v1/runs/{run_id}/artifacts/{artifact_id}/file endpoint (307 redirect).

Changes:

  • Add Run.get_artifact_download_url() to fetch presigned URLs by intercepting the 307 redirect.
  • Update artifact download code paths to use the new /file endpoint instead of artifact.download_url.
  • Regenerate OpenAPI client and docs to include the new endpoint and updated API schema/version.

Reviewed changes

Copilot reviewed 6 out of 50 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/aignostics/platform/resources/runs_test.py Adds unit tests for Run.get_artifact_download_url() redirect/headers/error behavior.
tests/aignostics/application/download_test.py Reformats imports after download flow changes.
src/aignostics/platform/resources/runs.py Implements /file-based presigned URL retrieval and updates run download logic to use it.
src/aignostics/application/_download.py Uses Run.get_artifact_download_url() to download artifacts instead of artifact.download_url.
codegen/out/docs/PublicApi.md Documents new GET /v1/runs/{run_id}/artifacts/{artifact_id}/file endpoint and updated API params.
codegen/out/aignx/codegen/rest.py Regenerated client metadata for OpenAPI 1.4.0.
codegen/out/aignx/codegen/models/version_read_response.py Regenerated model updates (incl. version regex validation).
codegen/out/aignx/codegen/models/validation_error_loc_inner.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/validation_error.py Regenerated model to include input/ctx fields.
codegen/out/aignx/codegen/models/user_read_response.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/scheduling_response.py New generated model for run scheduling response fields.
codegen/out/aignx/codegen/models/scheduling_request.py New generated model for run scheduling request fields.
codegen/out/aignx/codegen/models/run_termination_reason.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/run_state.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/run_read_response.py Adds scheduling field to run response model.
codegen/out/aignx/codegen/models/run_output.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/run_item_statistics.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/run_creation_response.py Updates run creation response schema (run_id now required).
codegen/out/aignx/codegen/models/run_creation_request.py Adds optional scheduling to run creation request.
codegen/out/aignx/codegen/models/output_artifact_visibility.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/output_artifact_scope.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/output_artifact_result_read_response.py Marks download_url deprecated in schema and reshuffles fields.
codegen/out/aignx/codegen/models/output_artifact.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/organization_read_response.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/me_read_response.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/item_termination_reason.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/item_state.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/item_result_read_response.py Adds error_code and adjusts required fields/order.
codegen/out/aignx/codegen/models/item_output.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/item_creation_request.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/input_artifact_creation_request.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/input_artifact.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/http_validation_error.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/custom_metadata_update_response.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/custom_metadata_update_request.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/artifact_termination_reason.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/artifact_state.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/artifact_output.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/application_version.py Adds version regex validation to generated model.
codegen/out/aignx/codegen/models/application_read_short_response.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/application_read_response.py Regenerated model header/version update.
codegen/out/aignx/codegen/models/init.py Regenerated export ordering and includes new scheduling models.
codegen/out/aignx/codegen/exceptions.py Regenerated client metadata for OpenAPI 1.4.0.
codegen/out/aignx/codegen/configuration.py Regenerated client metadata for OpenAPI 1.4.0 and debug report string.
codegen/out/aignx/codegen/api_client.py Regenerated client metadata for OpenAPI 1.4.0.
codegen/out/aignx/codegen/api/public_api.py Adds generated API method for /file endpoint and updates resource paths/params.
codegen/out/.openapi-generator/FILES Tracks newly generated files (scheduling models).
codegen/in/openapi.json Updates OpenAPI input spec to 1.4.0 and adds /file endpoint + schema updates.
codegen/in/archive/openapi_1.3.0+dev.305920c97bb.json Adds archived OpenAPI spec snapshot for 1.3.0+dev build.
CHANGELOG.md Removes trailing blank lines at end of file.

Comment on lines +272 to +282
def get_artifact_download_url(self, artifact_id: str) -> str:
"""Fetch a fresh presigned download URL for a specific output artifact.

Calls ``GET /v1/runs/{run_id}/artifacts/{artifact_id}/file``. The
endpoint responds with a ``307 Temporary Redirect`` whose ``Location``
header carries the presigned download URL. Because urllib3 follows
redirects automatically the URL is captured by intercepting the redirect
with ``requests`` (``allow_redirects=False``), using the endpoint URL
and auth headers obtained from the generated client's serializer so that
no path strings are hardcoded here.

Copilot AI review requested due to automatic review settings March 18, 2026 12:50
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the SDK to stop relying on the deprecated download_url field and instead fetch fresh presigned artifact URLs via the new GET /v1/runs/{run_id}/artifacts/{artifact_id}/file endpoint (redirect Location header). It also updates the generated OpenAPI client artifacts to API spec version 1.4.0.

Changes:

  • Add Run.get_artifact_download_url() and switch artifact downloads/previews to use the /file endpoint redirect Location.
  • Update GUI artifact preview/download actions to resolve fresh URLs on-demand.
  • Regenerate OpenAPI client code/docs for API 1.4.0 (new endpoint + scheduling models, various schema updates).

Reviewed changes

Copilot reviewed 8 out of 53 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/aignostics/platform/resources/runs.py Adds get_artifact_download_url() and switches artifact download flow to use fresh presigned URLs.
src/aignostics/application/_download.py Downloads item artifacts using a freshly resolved presigned URL instead of artifact.download_url.
src/aignostics/application/_gui/_page_application_run_describe.py GUI preview/download now resolves the presigned URL from the artifact id via the run API.
src/aignostics/application/_utils.py Removes printing of deprecated download_url in run item output.
tests/aignostics/platform/resources/runs_test.py Adds unit tests for get_artifact_download_url() behavior on redirects and error cases.
tests/aignostics/application/download_test.py Formatting-only import change.
codegen/in/openapi.json (+ archive) Updates OpenAPI spec input to 1.4.0 and archives it.
codegen/out/** Regenerated client/models/docs reflecting OpenAPI 1.4.0.
Makefile Tweaks codegen sed invocation (currently breaks GNU sed portability).
CHANGELOG.md Removes trailing blank lines at EOF.

# in codegen/out/public_api.py replace all occurrences of resource_path='/v1 with resource_path='/api/v1
# Use portable sed syntax: -i'' works on both macOS and Linux
sed -i"" "s|resource_path='/v1|resource_path='/api/v1|g" codegen/out/aignx/codegen/api/public_api.py
sed -i "" "s|resource_path='/v1|resource_path='/api/v1|g" codegen/out/aignx/codegen/api/public_api.py
Comment on lines +434 to 438
def csv_dialog_open(title: str, artifact_id: str) -> None:
"""Open the CSV dialog."""
csv_view_dialog_content.refresh(title=title, url=url)
download_url = Run.for_run_id(run_id).get_artifact_download_url(artifact_id)
csv_view_dialog_content.refresh(title=title, url=download_url)
csv_view_dialog.open()
Comment on lines +483 to 489
def tiff_dialog_open(title: str, artifact_id: str) -> None:
"""Open the TIFF dialog.

Args:
title (str): The title of the TIFF dialog.
url (str): The URL of the TIFF image.
artifact_id: (str): The ID of the artifact containing the TIFF to display.

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
2 New issues
65.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants